#include <Windows.h>
#include <process.h>
#include <cstdio>
#include "CLEyeMulticam.h"
#include "FreeImage.h"


/*  File-scope type delcarations  ****************************************************************/
typedef unsigned (__stdcall *ThreadFunc)( void* );


/*  File-scope function declarations  ************************************************************/
static DWORD WINAPI CaptureThread( LPVOID );


/*  File-scope variables and constants  **********************************************************/

// Commands
static const int INPUT_QUIT = 0;
static const int INPUT_CAPTURE_TOGGLE = 1;

// Capture thread
static volatile bool captureThreadQuit = false;
static volatile bool captureThreadCapture = false;
static HANDLE captureThreadEvent = CreateEvent( NULL, FALSE, FALSE, NULL );

// UI
static const wchar_t windowTitle[] = L"PSEyeCapture";


/*  Function definitions  ************************************************************************/

// The program entry point and message loop
int CALLBACK WinMain( HINSTANCE /* hInstance */,
                      HINSTANCE /* hInstancePrev */,
                      LPSTR     /* lpCmdLine */,
                      const int /* nCmdShow */ )
{
	// Initialize a Playstation EYE camera or quit if none are available.
	CLEyeCameraInstance camera = NULL;
	
	{
		const int camerasCount = CLEyeGetCameraCount();
		
		// Use the first camera that initilizes properly for 320x240 greyscale capture at 187.0fps.
		for( int cameraIndex = 0; cameraIndex < camerasCount; ++cameraIndex )
		{
			camera = CLEyeCreateCamera( CLEyeGetCameraUUID(cameraIndex),
			                            CLEYE_MONO_RAW,
			                            CLEYE_QVGA,
			                            187.0f );
			
			if( NULL != camera )
			{
				break;
			}
		}
		
		// Quit if no camera was initialized.
		if( NULL == camera )
		{
			wchar_t message[256] = L"";
			wsprintf( message, L"Unable to initialize any Playstation EYE cameras (found %i available).\n"
			                   L"Please make sure that one is connected and not already in use, then try again.",
			                   camerasCount );
			
			MessageBox( NULL, message, windowTitle, MB_OK );
			
			return EXIT_FAILURE;
		}
	}
	
	// Start the capture thread.
	const HANDLE captureThreadHandle = (HANDLE)_beginthreadex( NULL,
	                                                           0,
	                                                           (ThreadFunc)CaptureThread,
	                                                           (void*)&camera,
	                                                           0,
	                                                           NULL );
	
	// Register global hotkeys
	RegisterHotKey( NULL, 0, MOD_ALT | MOD_NOREPEAT, VK_ESCAPE );
	RegisterHotKey( NULL, 1, MOD_ALT | MOD_NOREPEAT, VK_BACK );
	
	// Main loop
	MSG message = { 0 };
	
	while( 0 != GetMessage(&message, NULL, 0, 0) )
	{
		// Process hotkey presses
		if( WM_HOTKEY == message.message )
		{
			if( INPUT_QUIT == message.wParam )
			{
				// Terminate the message loop.
				PostQuitMessage( EXIT_SUCCESS );
			}
			else if( INPUT_CAPTURE_TOGGLE == message.wParam )
			{
				// Toggle the camera capture state.
				captureThreadCapture = !captureThreadCapture;
				
				// Wake the thread it if it's time to start capturing.
				if( captureThreadCapture )
				{
					SetEvent( captureThreadEvent );
				}
			}
		}
	}
	
	// Signal the capture thread to stop capturing.
	captureThreadCapture = false;
	
	// Signal the capture thread to finish.
	captureThreadQuit = true;
	
	// Wake the capture thread (if it was suspended).
	SetEvent( captureThreadEvent );
	
	// Wait for the capture thread to finish.
	WaitForSingleObjectEx( captureThreadHandle, INFINITE, FALSE );
	
	// Release the capture thread handle and synchronization objects.
	CloseHandle( captureThreadHandle );
	CloseHandle( captureThreadEvent );
	
	// Release the camera.
	CLEyeDestroyCamera( camera );
	
	// Return the exit code supplied with the WM_QUIT message.
	return message.wParam;
}


/* A thread function that captures up to 10 seconds of 187fps images at a time and writes them to
   disk. The thread suspends itself after initializing and wakes to process commands when the
   captureThreadEvent object is signaled. After capturing, the thread will suspend itself again
   until given a command to capture again or to exit.
   
   To begin capturing, set the captureThreadCapture variable to true and signal captureThreadEvent.
   To stop and write images out, set captureThreadCapture to false and signal captureThreadEvent.
   To cause the thread to exit (after stopping and writing images), set captureThreadQuit to true
      and signal captureThreadEvent.
   
   Parameters:
   cameraPtr - A pointer to a valid CLEyeCameraInstance
*/ 
DWORD WINAPI CaptureThread( LPVOID cameraPtr )
{
	// Unpack the camera instance handle from the thread parameter.
	const CLEyeCameraInstance& camera = *(CLEyeCameraInstance*)cameraPtr;
	
	// Configure the camera for short exposure to reduce motion blur
	// and high gain because so little light will hit the sensor.
	CLEyeSetCameraParameter( camera, CLEYE_EXPOSURE, 32 );
	CLEyeSetCameraParameter( camera, CLEYE_GAIN, 64 );
	
	// Get the dimensions for the camera's captured image.
	int captureWidth = 0;
	int captureHeight = 0;
	CLEyeCameraGetFrameDimensions( camera, captureWidth, captureHeight );
	
	// Allocate a storage to hold a fixed number of captured images.
	struct CaptureData
	{
		FIBITMAP* image;
		FILETIME time;
	};
	
	const size_t captureFramesMax = 187 * 10;
	CaptureData captureFrames[captureFramesMax] = { 0 };
	
	for( size_t frameIndex = 0; frameIndex < captureFramesMax; ++frameIndex )
	{
		captureFrames[frameIndex].image = FreeImage_Allocate( captureWidth, captureHeight, 8 );
	}
	
	while( !captureThreadQuit )
	{
		// Wait for a command.
		WaitForSingleObject( captureThreadEvent, INFINITE );
		
		if( captureThreadCapture )
		{
			// Start the camera.
			CLEyeCameraStart( camera );
			
			// Sleep for a second.
			// TODO: Is this wait necessary? It was in the CL example.
			Sleep ( 1000 );
			
			OutputDebugString( L"Starting Capture\n" );
			
			// Turn the red camera LED on.
			CLEyeCameraLED( camera, true );
			
			// Capture frames until requested to stop.
			size_t framesIndex = 0;
			while( captureThreadCapture )
			{
				// Stop capturing if we're out of storage space.
				if( captureFramesMax <= framesIndex )
				{
					captureThreadCapture = false;
					break;
				}
				
				// Get the current frame.
				CaptureData& frameData = captureFrames[framesIndex];
				
				// Copy the frame from the camera sensor.
				CLEyeCameraGetFrame( camera, FreeImage_GetBits(frameData.image) );
				
				// Get the time of the capture.
				GetSystemTimeAsFileTime( &frameData.time );
				
				framesIndex += 1;
			}
			
			// Stop the camera.
			CLEyeCameraStop( camera );
			
			// Create a directory to hold the captured frames.
			char dirName[64] = "";
			{
				// Convert the first frame's UTC file time into local system time.
				FILETIME localTime = { 0 };
				FileTimeToLocalFileTime( &captureFrames[0].time, &localTime );
				
				SYSTEMTIME time = { 0 };
				FileTimeToSystemTime( &localTime, &time );
				
				// Format the local time into an easy-to-read string.
				sprintf_s( dirName, sizeof(dirName), "%d-%02d-%02d (%02d.%02d.%02d)", time.wYear, time.wMonth, time.wDay, time.wHour, time.wMinute, time.wSecond );
				
				// Create a directory named for the time stamp.
				CreateDirectoryA( dirName, NULL );
			}
			
			// Save the captured frames to disk.
			const size_t framesCount = framesIndex;
			for( framesIndex = 0; framesIndex < framesCount; ++framesIndex )
			{
				// Indicate progress by toggling the camera's red LED every so often.
				if( 0 == (framesIndex % 30) )
				{
					static bool cameraLEDToggle = true;
					cameraLEDToggle = !cameraLEDToggle;
					CLEyeCameraLED( camera, cameraLEDToggle );
				}
				
				// Get the current frame.
				CaptureData& frameData = captureFrames[framesIndex];
				
				// Captured frames are upside-down. Flip them around.
				FreeImage_FlipVertical( frameData.image );
				
				// Convert the frame's UTC time stamp into milliseconds since the Win32 epoch.
				ULARGE_INTEGER frameTime = { 0 };
				frameTime.HighPart = frameData.time.dwHighDateTime;
				frameTime.LowPart  = frameData.time.dwLowDateTime;
				
				const unsigned long long timeMS = (unsigned long long)(frameTime.QuadPart / 10000);
				const unsigned long timeMSFrac  = (unsigned long)(frameTime.QuadPart - (timeMS * 10000));
				
				// Use the time in milliseconds as the file name.
				char fileName[128] = "";
				sprintf_s( fileName, sizeof(fileName), "%s\\%lld.%04d.bmp", dirName, timeMS, timeMSFrac );
				
				// Write the image to disk.
				FreeImage_Save( FIF_BMP, frameData.image, fileName, BMP_DEFAULT );
			}
			
			// Turn the red camera LED off.
			CLEyeCameraLED( camera, false );
			
			OutputDebugString( L"Stopping Capture\n" );
		}
	}
	
	OutputDebugString( L"Stopping Thread\n" );
	
	// Release the frame storage.
	for( size_t frameIndex = 0; frameIndex < captureFramesMax; ++frameIndex )
	{
		FreeImage_Unload( captureFrames[frameIndex].image );
	}
	
	return EXIT_SUCCESS;
}
